﻿using System;
using System.IO;
using System.Net;
using System.ServiceModel.Syndication;
using System.ServiceModel.Web;
using System.Xml;
using NCMIS.ObjectModel;
using NCMIS.ObjectModel.MetaData;
using NCMIS.ObjectModel.Rest;
using NCMIS.Produce;
using NCMIS.Provider;
using NCMIS.ServiceModel.Syndication;
using System.Text;

namespace NCMIS.ServiceModel.Ra
{
    /// <summary>
    /// Provides the Navigation Services for the REST/Atom binding. The navigation services are used to traverse the folder hierarchy
    /// in a CMIS Repository, and to locate Documents that are checked out.
    /// </summary>
    public partial class Service : IService
    {
        #region Helpers

        // Removes unallowed characters in object name and ID
        private string RemoveUnallowedCharacters(string source)
        {
            string result = source.Replace(" ", string.Empty);
            result = result.Replace(".", string.Empty);
            return result;
        }

        #endregion

        /// <summary>
        /// Handles POST requests to Folder Children Collection.
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The id of the folder.</param>
        /// <param name="stream">The Atom entry holding the object to create.</param>
        /// <returns>The newly created object, formatted as an Atom 1.0 Entry.</returns>
        [WebInvoke(UriTemplate = "/{repositoryId}/children/{folderId}")]
        public Atom10ItemFormatter CreateChildren(string repositoryId, string folderId, Stream stream)
        {
            string sourceFolderId = RequestParameter.SourceFolderId;
            string targetFolderID = RequestParameter.TargetFolderId;
            VersioningState versioningState = RequestParameter.VersioningState;

            IncomingWebRequestContext ictx = WebOperationContext.Current.IncomingRequest;
            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;

            Atom10ItemFormatter itemFormatter = null;
            SyndicationItem syndicationItem;
            string contentType = ictx.ContentType;

            // Attempt to get the object to create as a syndication item
            using (XmlReader reader = XmlReader.Create(stream))
            {
                try
                {
                    syndicationItem = SyndicationItem.Load(reader);
                }
                catch (XmlException)
                {
                    ctx.StatusCode = ServiceException.InvalidArgument;
                    return null;
                }
            }

            // Convert the syndication item to a CMIS object
            CmisObject cmisObject = CmisSyndicationItem.GetObject(syndicationItem);

            // Extract object type ID and base type ID from the CMIS object
            string objectTypeId = string.Empty;
            string baseTypeId = string.Empty;
            if (cmisObject.Properties.ObjectTypeId != null)
            {
                objectTypeId = cmisObject.Properties.ObjectTypeId.SingleValue;
            }
            if (cmisObject.Properties.BaseTypeId != null)
            {
                baseTypeId = cmisObject.Properties.BaseTypeId.SingleValue;
            }

            ContentStream contentStream = null;
            string createdObjectId = string.Empty;

            // Since the ID is null, we must create the object depending on the object type
            if (string.IsNullOrEmpty(cmisObject.Id))
            {
                switch (objectTypeId)
                {
                    case "cmis:folder":
                        itemFormatter = CreateFolder(repositoryId, folderId, syndicationItem);      // Located in ObjectService
                        break;
                    case "cmis:document":
                        contentStream = CmisSyndicationItem.GetContentStream(syndicationItem);
                        string[] policies = cmisObject.PolicyIds != null ? cmisObject.PolicyIds.id : null;
                        // Add object properties
                        if (cmisObject.Properties.Name == null)
                        {
                            cmisObject.Properties.Add(new CmisPropertyString() 
                            { 
                                PropertyDefinitionId = CmisPropertyDefinitionId.Name, 
                                Value = new string[] {  RemoveUnallowedCharacters(syndicationItem.Title.Text) } 
                            });
                        }
                        CreateDocument(repositoryId, cmisObject.Properties, folderId, contentStream, versioningState, policies, null, null);
                        break;
                    case "cmis:policy":
                        itemFormatter = CreatePolicy(repositoryId, syndicationItem);
                        break;
                    default:
                        // The client has sent an erroneous object, expected objectTypeId is one of the above
                        break;
                }
            }
            else
            {
                // Object already exists (no source folder specified); add it to the specified folder
                if (string.IsNullOrEmpty(sourceFolderId))
                {
                    MultiFilingProviderManager.Provider.AddObjectToFolder(repositoryId, cmisObject.Id, folderId, true);
                }
                // Object already exists; move it to the specified folder
                else
                {
                    ObjectProviderManager.Provider.MoveObject(repositoryId, cmisObject.Id, folderId, sourceFolderId);
                }
            }

            ctx.StatusCode = HttpStatusCode.Created;

            return itemFormatter;
        }

        /// <summary>
        /// Gets the list of child objects contained in the specified folder. (Handles GET request to Folder Children Collection).
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The folder id of the folder to retreive descendants for.</param>
        /// <param name="maxItems">The maximum number of items to return in the response. (optional, default: repository specific)</param>
        /// <param name="skipCount">The number of potential results to skip before returning any results. (optional, default: 0)</param>
        /// <param name="orderBy">Specifies sort specification for returned result, must be a comma separated list of one or more column names. (Optional, default: empty)</param>
        /// <param name="filter">Value indicating which properties for Objects that must be returned. (Optional, default: Not set)</param>
        /// <param name="includeRelationships">Value indicating what relationships in which the objects returned participate that must be returned, if any. (Optional, default: none)</param>
        /// <param name="renditionFilter">Renditions whose kind matches this filter must be returned. (Optional, default: cmis:none)</param>
        /// <param name="includeAllowableActions">If true, then the the available actions for each object in the result set must be returned. (Optional, default: false)</param>
        /// <param name="includePathSegment">If true, then a PathSegment for each child object, for use in constructing that object’s path, must be returned. (Optional, default: false)</param>
        /// <returns>Child objects for specified folder, formatted as Atom 1.0 Feed.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/children/{folderId}")]
        public Atom10FeedFormatter GetChildren(string repositoryId, string folderId)
        {
            int? maxItems = RequestParameter.MaxItems;
            int skipCount = RequestParameter.SkipCount;
            string orderBy = RequestParameter.OrderBy;
            string filter = RequestParameter.Filter;
            IncludeRelationships includeRelationships = RequestParameter.IncludeRelationships;
            string renditionFilter = RequestParameter.RenditionFilter;
            bool includeAllowableActions = RequestParameter.IncludeAllowableActions;
            bool includePathSegment = RequestParameter.IncludePathSegment;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Feed;

            // Repository ID and folder ID must be specified
            if (String.IsNullOrEmpty(repositoryId) || String.IsNullOrEmpty(folderId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId and folderId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }
            // Store the children in a pathedCmisObjectList
            PathedCmisObjectList pathedCmisObjectList = NavigationProviderManager.Provider.GetChildren(repositoryId, folderId, maxItems, skipCount, orderBy, filter, includeRelationships, renditionFilter, includeAllowableActions, includePathSegment);

            // TODO: Change from SampleProvider
            string displayName = NCMIS.Provider.SampleProvider.SampleFactory.CreateTypeDefinition("cmis:folder").DisplayName;

            // Create a feed to hold the list content
            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = _baseUri,
                Id = String.Format("urn:uuid:{0}-children", folderId),
                LastUpdatedTime = new DateTimeOffset(DateTime.Now),
                RepositoryId = repositoryId,
                SelfLinkUri = new Uri(_baseUri, String.Format("{0}/children/{1}{2}", repositoryId, folderId, HttpRequest.GetQueryString())),
                ServiceLinkUri = new Uri(_baseUri, String.Format("{0}/servicedoc", repositoryId)),
                ViaLinkUri = new Uri(_baseUri, String.Format("{0}/object/{1}", repositoryId, folderId)),
                Title = new TextSyndicationContent(displayName) // atom:title MUST be the displayName attribute of the CMIS Type Definition
            };
            feed.Authors.Add(CmisRestAtom.Author);
            if (pathedCmisObjectList != null)
            {
                feed.AddEntries(pathedCmisObjectList);
            }
            feed.AddNumItems();

            // Format the feed as an Atom feed
            return new Atom10FeedFormatter(feed);
        }
        
        /// <summary>
        /// Gets the set of descendant objects contained in the specified folder or any of its child-folders.
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The folder id of the folder to retreive descendants for.</param>
        /// <param name="depth">The number of levels of depth in the type hierachy from which to return results. (Optional, default: repository specific)</param>
        /// <param name="filter">Value indicating which properties for Objects that must be returned. (Optional, default: Not set)</param>
        /// <param name="includeRelationships">Value indicating what relationships in which the objects returned participate that must be returned, if any. (Optional, default: none)</param>
        /// <param name="renditionFilter">Renditions whose kind matches this filter must be returned. (Optional, default: cmis:none)</param>
        /// <param name="includeAllowableActions">If true, then the the available actions for each object in the result set must be returned. (Optional, default: false)</param>
        /// <param name="includePathSegment">If true, then a PathSegment for each child object, for use in constructing that object’s path, must be returned. (Optional, default: false)</param>
        /// <returns>Descendant objects for specified folder, formatted as Atom 1.0 Feed.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/descendants/{folderId}")]
        public Atom10FeedFormatter GetDescendants(string repositoryId, string folderId)
        {
            int? depth = RequestParameter.Depth;
            string filter = RequestParameter.Filter;
            IncludeRelationships includeRelationships = RequestParameter.IncludeRelationships;
            string renditionFilter = RequestParameter.RenditionFilter;
            bool includeAllowableActions = RequestParameter.IncludeAllowableActions;
            bool includePathSegment = RequestParameter.IncludePathSegment;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Feed;

            // Repository ID and folder ID must be specified
            if (String.IsNullOrEmpty(repositoryId) || String.IsNullOrEmpty(folderId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId and folderId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }

            // Store the descendants in a PathedCmisObjectContainer array
            PathedCmisObjectContainer[] folderTree = NavigationProviderManager.Provider.GetDescendants(repositoryId, folderId, depth, filter, includeRelationships,
                renditionFilter, includeAllowableActions, includePathSegment);

            // Create a feed to hold the array content
            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = _baseUri,
                Id = "urn:uuid:descendants",
                RepositoryId = repositoryId,
                SelfLinkUri = new Uri(_baseUri, String.Format("{0}/typechildren", repositoryId)),
                ServiceLinkUri = new Uri(_baseUri, String.Format("{0}/servicedoc", repositoryId)),
                Title = new TextSyndicationContent("Descendants"),
            };
            feed.AddEntries(folderTree);
            feed.AddNumItems();

            // Format the feed as an Atom feed
            return new Atom10FeedFormatter(feed);
        }

        /// <summary>
        /// Gets the set of descendant folder objects contained in the specified folder. Exposes the GET method for the Folder Tree Feed;
        /// a hierarchical feed comprising all the folders under a specified folder. Available via the link relation foldertree from the service document.
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The folder id of the folder to retreive tree for.</param>
        /// <param name="depth">The number of levels of depth in the type hierachy from which to return results. (Optional, default: repository specific)</param>
        /// <param name="filter">Value indicating which properties for Objects that must be returned. (Optional, default: Not set)</param>
        /// <param name="includeRelationships">Value indicating what relationships in which the objects returned participate that must be returned, if any. (Optional, default: none)</param>
        /// <param name="renditionFilter">Renditions whose kind matches this filter must be returned. (Optional, default: cmis:none)</param>
        /// <param name="includeAllowableActions">If true, then the the available actions for each object in the result set must be returned. (Optional, default: false)</param>
        /// <param name="includePathSegment">If true, then a PathSegment for each child object, for use in constructing that object’s path, must be returned. (Optional, default: false)</param>
        /// <returns>Descendant folder objects, for the specified folder, formatted as Atom 1.0 Feed.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/foldertree/{folderId}")]
        public Atom10FeedFormatter GetFolderTree(string repositoryId, string folderId)
        {
            int? depth = RequestParameter.Depth;
            string filter = RequestParameter.Filter;
            IncludeRelationships includeRelationships = RequestParameter.IncludeRelationships;
            string renditionFilter = RequestParameter.RenditionFilter;
            bool includeAllowableActions = RequestParameter.IncludeAllowableActions;
            bool includePathSegment = RequestParameter.IncludePathSegment;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Feed;

            // Repository ID and folder ID must be specified
            if (String.IsNullOrEmpty(repositoryId) || String.IsNullOrEmpty(folderId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId and folderId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }

            // Store the descendants in a PathedCmisObjectContainer array
            PathedCmisObjectContainer[] folderTree = NavigationProviderManager.Provider.GetFolderTree(repositoryId, folderId, depth, filter, includeRelationships,
                renditionFilter, includeAllowableActions, includePathSegment);

            // Create a feed to hold the array content
            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = _baseUri,
                Id = String.Format("urn:uuid:{0}-foldertree", folderId),
                RepositoryId = repositoryId,
                SelfLinkUri = new Uri(_baseUri, String.Format("{0}/foldertree/{1}", repositoryId, folderId)),
                ServiceLinkUri = new Uri(_baseUri, String.Format("{0}/servicedoc", repositoryId)),
                Title = new TextSyndicationContent("Folder Tree"),
                ViaLinkUri = new Uri(_baseUri, String.Format("{0}/object/{1}", repositoryId, folderId))
            };

            // TODO: add down link (tree)
            //       add http://docs.oasis-open.org/ns/cmis/link/200908/foldertree link
            //       add first, last links (paging)

            feed.AddEntries(folderTree);
            feed.AddNumItems();

            // Format the feed as an Atom feed
            return new Atom10FeedFormatter(feed);
        }

        /// <summary>
        /// Gets the parent folder object for the specified folder object.
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The folder id to retreive parent folder object for.</param>
        /// <param name="filter">Value indicating which properties for Objects that must be returned.</param>
        /// <returns>Parent folder object for specified folder, formatted as Atom 1.0 Entry.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/object/{folderId}/parent")]
        public Atom10ItemFormatter GetFolderParent(string repositoryId, string folderId)
        {
            string filter = RequestParameter.Filter;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Entry;

            // Repository ID and folder ID must be specified
            if (String.IsNullOrEmpty(repositoryId) || String.IsNullOrEmpty(folderId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId and folderId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }

            // Store the retrieved parent as a CmisObject
            CmisObject cmisObject = NavigationProviderManager.Provider.GetFolderParent(repositoryId, folderId, filter);

            // Convert parent to a syndication item
            CmisSyndicationItem entry = CmisSyndicationItem.CreateEntry(repositoryId, cmisObject);

            // Format parent entry as an Atom entry
            return new Atom10ItemFormatter(entry);
        }

        /// <summary>
        /// Gets the parent folder(s) for the specified non-folder, fileable object. Exposes the GET method for the Objects Parents Feed.
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="objectId">The id of the object.</param>
        /// <returns>Parent objects, formatted as Atom 1.0 Feed.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/objectparents/?objectId={objectId}&filter={filter}&includeRelationships={includeRelationships}&renditionFilter={renditionFilter}&includeAllowableActions={includeAllowableActions}&includeRelativePathSegment={includeRelativePathSegment}")]
        public Atom10FeedFormatter GetObjectParents(string repositoryId, string objectId, string filter, string includeRelationships, string renditionFilter,
            bool includeAllowableActions, bool includeRelativePathSegment)
        {
            IncludeRelationships _includeRelationships = IncludeRelationships.None;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Feed;

            // Repository ID must be specified
            if (String.IsNullOrEmpty(repositoryId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }

            // Store the object parents in a PathedCmisObject array
            PathedCmisObject[] objectParents = NavigationProviderManager.Provider.GetObjectParents(repositoryId, objectId, filter, _includeRelationships,
                renditionFilter, includeAllowableActions, includeRelativePathSegment);

            // Create a feed to hold the array content
            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = _baseUri,
                Id = "urn:uuid:checkedout",
                RepositoryId = repositoryId,
                SelfLinkUri = new Uri(_baseUri, String.Format("{0}/checkedout", repositoryId)),
                ServiceLinkUri = new Uri(_baseUri, String.Format("{0}/servicedoc", repositoryId)),
                Title = new TextSyndicationContent("Checkedout Documents"),
            };
            feed.AddEntries(objectParents);
            feed.AddNumItems();

            // Format feed as an Atom feed
            return new Atom10FeedFormatter(feed);
        }

        /// <summary>
        /// Gets the checked out documents. Exposes the GET method for the Checkedout Feed. Available via the Checkedout collection in the service document. 
        /// </summary>
        /// <param name="repositoryId">The id of the repository.</param>
        /// <param name="folderId">The folder id of the folder to retreive checkedout documents for. If left out all checked out documents will be returned. (Optional, default: empty)</param>
        /// <param name="filter">Value indicating which properties for Objects that must be returned. (Optional, default: Not set)</param>
        /// <param name="orderBy">Specifies sort specification for returned result, must be a comma separated list of one or more column names. (Optional, default: empty)</param>
        /// <param name="includeAllowableActions">If true, then the the available actions for each object in the result set must be returned. (Optional, default: false)</param>
        /// <param name="includeRelationships">Value indicating what relationships in which the objects returned participate that must be returned, if any. (Optional, default: none)</param>
        /// <param name="renditionFilter">Renditions whose kind matches this filter must be returned. (Optional, default: cmis:none)</param>
        /// <param name="maxItems">The maximum number of items to return in the response. (optional, default: repository specific)</param>
        /// <param name="skipCount">The number of potential results to skip before returning any results. (optional, default: 0)</param>
        /// <returns>Checked out documents, formatted as Atom 1.0 Feed.</returns>
        [WebGet(UriTemplate = "/{repositoryId}/checkedout")]
        public Atom10FeedFormatter GetCheckedoutDocs(string repositoryId)
        {
            string folderId = RequestParameter.FolderId;
            string filter = RequestParameter.Filter;
            string orderBy = RequestParameter.OrderBy;
            bool includeAllowableActions = RequestParameter.IncludeAllowableActions;
            IncludeRelationships includeRelationships = RequestParameter.IncludeRelationships;
            string renditionFilter = RequestParameter.RenditionFilter;
            int? maxItems = RequestParameter.MaxItems;
            int skipCount = RequestParameter.SkipCount;

            OutgoingWebResponseContext ctx = WebOperationContext.Current.OutgoingResponse;
            ctx.ContentType = CmisContentType.Feed;

            // Repository ID must be specified
            if (String.IsNullOrEmpty(repositoryId))
            {
                ctx.StatusCode = ServiceException.InvalidArgument;
                ctx.StatusDescription = String.Format("{0}: The parameters repositoryId must not be null or empty.",
                    ServiceException.InvalidArgument.ToString());
                return null;
            }

            // Store the checked out docs in a CmisObjectList
            CmisObjectList checkedOutDocs = NavigationProviderManager.Provider.GetCheckedoutDocs(repositoryId, folderId, filter, orderBy, includeAllowableActions, includeRelationships, renditionFilter, maxItems, skipCount);

            // Create a feed to hold the list content
            CmisSyndicationFeed feed = new CmisSyndicationFeed()
            {
                BaseUri = _baseUri,
                Id = "urn:uuid:checkedout",
                RepositoryId = repositoryId,
                SelfLinkUri = new Uri(_baseUri, String.Format("{0}/checkedout{1}", repositoryId, HttpRequest.GetQueryString())),
                ServiceLinkUri = new Uri(_baseUri, String.Format("{0}/servicedoc", repositoryId)),
                Title = new TextSyndicationContent("Checkedout Documents"),
            };
            feed.AddEntries(checkedOutDocs);

            // Format the feed as an Atom feed
            return new Atom10FeedFormatter(feed);
        }
    }
}
